Piping Server上でエンドツーエンド暗号化チャット
Piping Serverを経由して、エンドツーエンド暗号化(E2EE)でセキュアにチャット。
デモ動画
https://youtu.be/moTkX_me4Ak
デモの動画の通り、以下のようなことができるようになっています。
リアルタイムにメッセージがやりとり
エンドツーエンドでRSAを用いた暗号化を行う
日本語や絵文字も送信できる
鍵のビット長も設定で強化可能
経由するPiping Serverを変更可能
他のツール生成した公開鍵・秘密鍵を使用可能
openssl genrsa -out private.pem 4096
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
Piping Chat
GitHubリポジトリ
以下のリポジトリに実装がある。Vue + TypeScriptで書かれている。
静的サイトしてホスト可能で、GitHub PagesやAWS S3やNetlifyなどでホストできる。
https://gh-card.dev/repos/nwtgck/piping-chat-web.svg https://github.com/nwtgck/piping-chat-web
思っていること
このチャットは、Proof of Concept的なもの。Piping Serverはコンピュータ上に存在できるあらゆるデータを転送可能にすることが目的。だから、「チャットも実現可能だよ」ってことを示すための実装のつもり。 この実装だけが唯一のPiping Sever経由のチャットになる必要がない。色んな人がPiping Serverを経由してチャットを実現して問題なく、より安全な通信が確率できるネット界の実現ができたらいいなってこと。
設計とか実装とか
安全性に関しての設計のときに思ったこととか、それの実装について。
エンドツーエンド暗号化の必要性
前提として、HTTPSでPiping Serverで接続していれば、クライアント<=>サーバー間の通信は保護されている。
ただ、サーバー内ではデータがHTTPSが暗号が復号される。そのため、経由するPiping Serverが悪意のあったりすれば、チャット内容が奪われる可能性がある。そのため、エンドツーエンドでの暗号化が安全性のために必要になる。
ブラウザで暗号化する手法
Android/iOS/PCに限らず動かすために、ブラウザで動く言語や技術を選んだ。実装はVue/TypeScriptで行った。ブラウザ上で、RSAの公開鍵暗号を使いたかった。JSEncryptを使って鍵生成や暗号化/複号を行う。 チャットは1-対-1。互いにページを開くと非同期で鍵生成される。connectボタンを押すと公開鍵の交換をする。交換終了後、接続確立し、相手に送るときは相手の公開鍵で暗号化する。相手から受け取るときは、自分の秘密鍵で復号する。
Piping Serverの使われ方
Piping Serverをどうやって使うかは、チャット作成者によって変わっていいと思う。ここではあくまでも今回はこんな実装になってます程度の話。
現在のところの実装の説明をする。具体例として、AさんとBさんがチャットしたい。雰囲気としては、AさんはPOST /A-to-BでBさんへの送信。GET /B-to-AでBさんから受け取るようにする。同様に、BさんはPOST /B-to-AとGET /A-to-Bをする。
実際のパス生成は、sha256("${id}-to-${peerId}")の擬似コードのようになる。
SHA256をするメリットは、
Piping Server上でアクセスログを取っていた場合、サーバー運営に対しIDを隠すことができる。
SHA256をブルートフォースできるが、IDには日本語なども含められるため、結構困難のはず
サーバー運営者によるなりすましの可能性
Piping Severの運営者が悪意がある場合の話(またはサーバーに脆弱性があり、それを悪用する人がいた場合の話)。
エンドツーエンドで暗号化だけでは完璧に安全とはいえない。
GET path, POST pathのpathは暗号化されないため、pathをしれるサーバー運営者がそのpathに対してなりすまして、接続を確立させることができる。これを解決するためには、「通信相手が本当に通信したい相手なのか」を確かめることで、解決できるはず。相手の公開鍵の情報を事前に取得しておいて、それと等しいか確認する方法が一般的な気がする。公開鍵のフィンガープリントを使ってもいいと思う。
なにか安全かつ手軽に相手を認証する方法を模索中。
Piping Chatの拡張性
実装内でparcelの呼んでいるものの拡張性に関して。
parcelはPiping ServerにPOSTしたりGETしたときのHTTPボディになるJSON。
具体的なparcelは{kind: "talk", content: "..."}とか{kind: "rsa_key", content: "..."}とか。
なぜJSONにしたのかに関しては、相手といろんな通信に対応しやすくするため。
例えば、
RSAのキーを安全性のために、自動で再生成してそれを更新する命令
経由するPiping Serverを変更したりする命令(別に互いに違うまたは複数台Piping Serverを経由させてもいいと思う)
負荷分散
サーバーを分散させて万が一の場合でも、影響を小さくすることもできそう
そういう拡張を考えたときにJSONになった。添付ファイル的なバイナリがcontentのところに来るのは、あまり相性が良くないから、JSONにするかどうか迷ったが、そのときはバイナリ用にPiping SeverからGETできるpathをcontentを使って伝えるなどのことも可能なため、これでいいという判断になった。
今のところ、1-対-1のチャット。ここに関してはn人グループのような拡張ができることを考慮することを積極的に考えて設計しなかった。最初に作ったPiping Server経由のチャットなので、とりあえずシンプルな1-対-1を作ることを優先した。?n=数値でn人に向かって同じ内容を転送する機能などもあるので、使えるかもしれない(このときの暗号化・復号はGPGが複数人に向かって送信をやっていたと思うので、参考になりそう)。
HTTP/2
現在の実装でも複数のリクエストをPiping Serverに出している状態が存在する。